【知识回顾】Cobalt Strike 4.0 认证及修补过程

  • 想要理解整个认证过程,必须要动手走一轮代码。
  • 想要理解整个认证过程,必须要动手走一轮代码。
  • 想要理解整个认证过程,必须要动手走一轮代码。
  • 想要理解整个认证过程,必须要动手走一轮代码。
  • 想要理解整个认证过程,必须要动手走一轮代码。
  • 想要理解整个认证过程,必须要动手走一轮代码。

该文章在星球里吃了几个月的灰,后来看到 SleevedKey 在网上已经可以随便搜到,所以就索性放出来了。
已先投稿先知社区,链接为:https://xz.aliyun.com/t/8557

0x00 前言

这里提供了 CS 4.0 的认证过程,个人认为非常详细,文中配备认证的流程图,可以结合文中的代码注释,外加自己的 IDEA 调试,可以完整理解整个过程。因为 4.0 与 4.1 差了一个关键 key(前期的处理方式也多了一个步骤),因此这里就只针对 4.0 版本的认证进行说明。 附件中提供了 CSHook.jar,是针对 CS 4.1 版本的,并且文章中也明确提供了适用于 CS 4.1 的完整 key(使用该 key 需要删除多余的步骤,直接使用 4.0 的验证)。

很多人拿到原版之所以没有搞破解,是因为缺少了最重要的 Sleeved 解密 key。 其实,到了 4.X 版本,是没有办法进行”破解”的,因为 AES 的密钥是无法进行破译,所以 Sleeved 解密 key 只能等好心人提供。

CobaltStrike 4.X 的认证,如果对 Java 及密码学相关有所了解,理解起来并不难。但是对于它的破解来说,需要一个针对 Sleeved 模块的认证 key,这个 key 是无法进行穷举的,除非想不开了。因此对于破解来说,与其说破解,还不如说是将 key 进行补全了。

0x01 准备工作

1.1、必备知识

1.1.1、RAS 算法之加密与签名的区别

加密和签名都是为了安全性考虑,但略有不同。常有人问加密和签名是用私钥还是公钥?其实都是对加密和签名的作用有所混淆。简单的说,加密是为了防止信息被泄露,而签名是为了防止信息被篡改。这里举 2 个例子说明。

  • 第一个场景:战场上,B 要给 A 传递一条消息,内容为某一指令。

RSA 的加密过程如下:

1
2
3
1)A 生成一对密钥(公钥和私钥),私钥不公开,A 自己保留。公钥为公开的,任何人可以获取。
2)A 传递自己的公钥给 B,B 用 A 的公钥对消息进行加密。
3)A 接收到 B 加密的消息,利用 A 自己的私钥对消息进行解密。

  在这个过程中,只有 2 次传递过程,第一次是 A 传递公钥给 B,第二次是 B 传递加密消息给 A,即使都被敌方截获,也没有危险性,因为只有A的私钥才能对消息进行解密,防止了消息内容的泄露。

  • 第二个场景:A 收到 B 发的消息后,需要进行回复“收到”。

RSA 签名的过程如下:

1
2
3
1)A 生成一对密钥(公钥和私钥),私钥不公开,A 自己保留。公钥为公开的,任何人可以获取。
2)A 用自己的私钥对消息加签,形成签名,并将加签的消息和消息本身一起传递给 B。
3)B 收到消息后,在获取 A 的公钥进行验签,如果验签出来的内容与消息本身一致,证明消息是 A 回复的。

  在这个过程中,只有 2 次传递过程,第一次是 A 传递加签的消息和消息本身给 B,第二次是 B 获取 A 的公钥,即使都被敌方截获,也没有危险性,因为只有 A 的私钥才能对消息进行签名,即使知道了消息内容,也无法伪造带签名的回复给 B,防止了消息内容的篡改。

  但是,综合两个场景你会发现,第一个场景虽然被截获的消息没有泄露,但是可以利用截获的公钥,将假指令进行加密,然后传递给 A。第二个场景虽然截获的消息不能被篡改,但是消息的内容可以利用公钥验签来获得,并不能防止泄露。所以在实际应用中,要根据情况使用,也可以同时使用加密和签名,比如 A 和 B 都有一套自己的公钥和私钥,当 A 要给 B 发送消息时,先用 B 的公钥对消息加密,再对加密的消息使用 A 的私钥加签名,达到既不泄露也不被篡改,更能保证消息的安全性。

  总结:公钥加密、私钥解密;私钥签名、公钥验签。

但是,有一个要注意的是:

1
2
当你用公钥加密的时候,需要用私钥解密。
当你用私钥加密的时候,需要用公钥解密。
1.1.2、HMAC 消息摘要算法

MAC,全称 Message Authentication Code,也称为消息认证码(带密钥的Hash函数),通信实体双方使用的一种验证机制,保证消息数据完整性的一种工具。

在发送数据之前,发送方首先使用通信双方协商好的散列函数计算其摘要值。在双方共享的会话密钥作用下,由摘要值获得消息验证码。之后,它和数据一起被发送。接收方收到报文后,首先利用会话密钥还原摘要值,同时利用散列函数在本地计算所收到数据的摘要值,并将这两个数据进行比对。若两者相等,则报文通过认证。

说白了就是计算摘要的时候,需要一个秘钥 key,没有秘钥 key 就无法计算

1.1.3、AES
  • 破解 AES 算法需要多长时间?

以 AES-128 算法为例,平均需要尝试 2^127 ≈ 1.7*10^38 个 128bit 的随机数作为密钥进行加解密运算,方能找到正确的密钥。

常言道,“天下武功,唯快不破”;反之,天下密码,快必可破。问题是,那得有多快?我们知道,比特币网络在全球范围内调用了非常庞大的硬件资源以达到极高的运算效率,每秒钟操作的 Hash 运算(SHA-256)可高达 2.5644*10^19次。虽然 AES 和 SHA-256 算法并不相同,运算量也有所差异,但我们不妨近似地用该数据估算全球人民众志成城破解 AES 算法所需要的时间。

假设 AES 的运算效率为 2.564410^19 ≈ 2^64.4753 次/秒,则进行 2^127 次 AES 运算所需要的时间为:
2^127 / 2^64.4753 ≈ 2^62.5247秒 ≈ 6.6345
10^18 秒 ≈ 1.8429 10^15 小时 ≈ 7.6789 10^13 天 ≈ 2.104 * 10^11年 ≈ 210,400,000,000 年

1.2、运行环境

此次破解测试使用的工具及文件为:

  • IntelliJ IDEA Community Edition 2020.1.4
  • Feb 22, 2020 - Cobalt Strike 4.0

使用过 IDEA 的朋友都知道,它具备反编译 Jar 包的能力。

首先,我们使用 IDEA 新建一个工程,将原始 Jar 包作为依赖进行导入,如下图所示:

此时 IDEA 将调用反编译模块,因此我们可以直接查看 jar 的源码,如图所示:

但由于单个文件点击,并不利于我们的有效查看,因此可以提取 IDEA 的反编译功能,用于对原始 Jar 包的反编译。

下面我们进行测试,IDEA 的反编译功能依赖于 java-decompiler.jar ,该文件存在于以下路径当中:

1
%IDEA安装目录%\plugins\java-decompiler\lib\java-decompiler.jar

其使用方法为:

1
java -cp java-decompiler.jar org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true c:\my.jar d:\decompiled\

将反编译后的 Jar 包进行解压,将解压的文件(带文件夹)放入 src(该步骤仅将需要更改的文件放入即可,当然,全部放也没关系),文件夹内,如下图所示:

然后就是设置编译生成 Jar 的步骤。

Main Class 中填写 aggressor.Aggressor,其余默认即可;然后尝试 Build Artifacts...,正常情况下,则生成一个新的 Jar 包。

最后,为了方便实时预览及调试,我们需要对 Run 进行简单设置。

1
2
3
4
5
6
7
8
1.新建一个 run 配置
2.添加 JAR Application
3.选择运行的 Jar 包路径
4.配置启动该 Jar 包的虚拟选项
5.选择一个在执行 Run 操作时附带的操作
6.此处选择重新 Build Artiface

注意:如果不选择 5-6 步骤,则在点击 Run 前,需要手动 Build Artiface。

实践一下,是否配置都正常。出现以下信息就说明可行。

这部分内容不理解的朋友,可以去看看红队学院(知识星球)相关视频:RedCore 红队学院 CSTips

0x02 CS 3.X 版本的认证过程

其实,我们可以从头开始走一轮认证代码,3.X 相对简单,走下来其实不难。

主要涉及的文件:

1
2
3
4
common/License  // license 检查逻辑
common/Authorization // 检查的细节实现
common/AuthCrypto // RSA 解密和解压
common/CommonUtils // 相关数据类型转换辅助

先粗略说一下 3.X 的 .auth 整个加密过程是:

1
2
3
先对文本进行压缩,转换为 byte
添加特征头 0xca, 0xfe, 0xc0, 0xbb,0x00, 0x43
使用 RSA 进行加密

故此解密的话只需要逆向此流程即可,那么我们要伪造一个自己的授权文件的话,只需要把公钥替换为自己的,然后使用自己的私钥对文本内容进行加密即可。因为只有在验证 GUI 和 Console 的时候需要进行验证步骤,因此也可以直接写死 isValid()isPerpetual()isAlmostExpired() 的值。比如:

1
2
3
4
5
6
7
8
public Authorization() {
this.valid = true;
this.validto = "forever";
this.licensekey = "Cartier";
this.watermark = 1;
MudgeSanity.systemDetail("valid to", "perpetual");
MudgeSanity.systemDetail("id", this.watermark + "");
}

4.0 相比于 3.14 版本,多了一轮新的验证及更为复杂。

0x03 CS 4 .X 版本的认证过程

之所以只说这个 CS 4.x 的认证过程,是因为该认证是在 3.X 的基础上进行改进的。

首先从主函数开始查看,第一步认证:License.checkLicenseGUI(new Authorization());,我们在查看源码过程中,直接对源码进行注释即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Aggressor {
public static final String VERSION = "4.0 (20200222) " + (License.isTrial() ? "Trial" : "Licensed");
public static MultiFrame frame = null;

public static MultiFrame getFrame() {
return frame;
}

public static void main(String[] var0) {
ParserConfig.installEscapeConstant('c', "\u0003");
ParserConfig.installEscapeConstant('U', "\u001f");
ParserConfig.installEscapeConstant('o', "\u000f");
(new UseSynthetica()).setup();
Requirements.checkGUI();
// 认证开始
License.checkLicenseGUI(new Authorization());
frame = new MultiFrame();
(new ConnectDialog(frame)).show();
}
}

3.1、checkLicenseGUI()

该函数是 CobaltStrike 的第一道验证,主要检查授权文件是否存在、解析的数据是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void checkLicenseGUI(Authorization var0) {
// 判断文件是否存在、有效,格式是否正确等,isValid 函数是一个 flag,默认为 false
if (!var0.isValid()) {
CommonUtils.print_error("Your authorization file is not valid: " + var0.getError());
JOptionPane.showMessageDialog((Component)null, "Your authorization file is not valid.\n" + var0.getError(), (String)null, 0);
System.exit(0);
}
// 判断是否过期
if (!var0.isPerpetual()) {
if (var0.isExpired()) {
CommonUtils.print_error("Your Cobalt Strike license is expired. Please contact sales@strategiccyber.com to renew. If you did renew, run the update program to refresh your authorization file.");
JOptionPane.showMessageDialog((Component)null, "Your Cobalt Strike license is expired.\nPlease contact sales@strategiccyber.com to renew\n\nIf you did renew, run the update program to refresh your\nauthorization file.", (String)null, 0);
System.exit(0);
}
// 计算有效期
if (var0.isAlmostExpired()) {
CommonUtils.print_warn("Your Cobalt Strike license expires in " + var0.whenExpires() + ". Email sales@strategiccyber.com to renew. If you did renew, run the update program to refresh your authorization file.");
JOptionPane.showMessageDialog((Component)null, "Your Cobalt Strike license expires in " + var0.whenExpires() + "\nEmail sales@strategiccyber.com to renew\n\nIf you did renew, run the update program to refresh your\nauthorization file.", (String)null, 1);
}

}
}

首先是对 GUI 的一个验证,该验证有且仅有一次验证;它是调用 Authorization 类 中的 isValid()isPerpetual()isAlmostExpired()进行校验。

1
2
3
4
5
isValid()  // 判断文件是否存在、有效,格式是否正确等,isValid函数是一个flag,默认为false
isPerpetual() // 判断 forever 关键字是否存在,存在则结束函数
isAlmostExpired() - // 计算有效期

注:这三个函数方法的返回可直接写死,以绕过验证。

而它们都依赖于 Authorization.Authorization(),因此我们需要先对 Authorization() 进行分析。

3.2、Authorization() 的分析过程

Authorization 类中,我们只需要查看 Authorization() 函数即可。

该函数主要解析授权码字段构成和有效期的计算,并且调用了 AuthCrypto 类的 decrypt 函数对文件进行解密,详细信息请看代码注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public Authorization() {
// 读取当前目录中的 cobaltstrike.auth 文件
String str = CommonUtils.canonicalize("cobaltstrike.auth");
// 判断文件是否存在
if (!(new File(str)).exists()) {
try {
File localFile = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
if (localFile.getName().toLowerCase().endsWith(".jar")) {
localFile = localFile.getParentFile();
}
str = (new File(localFile, "cobaltstrike.auth")).getAbsolutePath();
} catch (Exception localException1) {
// 未找到该用于身份验证文件
MudgeSanity.logException("trouble locating auth file", localException1, false);
}
}

// 以 byte[]方式读取 cobaltstrike.auth 文件内容
byte[] arrayOfByte1 = CommonUtils.readFile(str);
// 判断长度,取决于文件内容
if (arrayOfByte1.length == 0) {
this.error = "Could not read " + str;
} else {
// 初始化 AuthCrypto 类,并在初始化时调用 load(),以校验 authkey.pub 文件是否符合要求
AuthCrypto authCrypto = new AuthCrypto();
// 调用 AuthCrypto 类中的 decrypt 方法对 cobaltstrike.auth 文件内容进行解密,校验文件是否符合要求,并返回 byte
byte[] arrayOfByte2 = authCrypto.decrypt(arrayOfByte1);
if (arrayOfByte2.length == 0) {
this.error = authCrypto.error();
} else {
try {
// 相比 3.14 版本,多了个 DateaParser,是用于解析 byte 类型数据的类
DataParser dataParser = new DataParser(arrayOfByte2);
dataParser.big();
// 该值是用于判断是否永久有效(是否为发行版)
int i = dataParser.readInt();
// 该值应该是水印作用。该值如果为 0,则在生成的 shellcode 中会带入 cs 水印(common/ListnerConfig.class)
this.watermark = dataParser.readInt();
// 该值是用于判断认证是否用于 Cobalt Strike 4.0+
byte j = dataParser.readByte();
// 取 16 个字节
byte k = dataParser.readByte();
// 获取关键 key,该 key是用于解密 Sleeved 的关键。
byte[] arrayOfByte3 = dataParser.readBytes(k);
if (j < 40) {
this.error = "Authorization file is not for Cobalt Strike 4.0+";
return;
}
if (i == 29999999) {
// 判断是否为 forever
this.validto = "forever";
MudgeSanity.systemDetail("valid to", "perpetual");
} else {
// 否则跳到试用期为20天
this.validto = "20" + i;
CommonUtils.print_stat("Valid to is: '" + this.validto + "'");
MudgeSanity.systemDetail("valid to", CommonUtils.formatDateAny("MMMMM d, YYYY", this.getExpirationDate()));
}

this.valid = true;
MudgeSanity.systemDetail("id", this.watermark + "");
// 4.0 的 key 为 {27, -27, -66, 82, -58, 37, 92, 51, 85, -114, -118, 28, -74, 103, -53, 6}
SleevedResource.Setup(arrayOfByte3);
} catch (Exception localException2) {
MudgeSanity.logException("auth file parsing", localException2, false);
}
}
}
}

3.3、AuthCrypto 类

看看对 authkey.pubcobaltstrike.auth 解密的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/*
* 解密函数解析,主要是涉及 RSA 解密和 gzip 解压相关操作,这里作者其实是在文件头加了四个字节
* */
public final class AuthCrypto {
public Cipher cipher;
public Key pubkey = null;
protected String error = null;

public AuthCrypto() {
try {
// 构造方法中生成了一个 RSA/ECB/PKCS1Padding 的 ciper
this.cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
this.load();
} catch (Exception var2) {
this.error = "Could not initialize crypto";
MudgeSanity.logException("AuthCrypto init", var2, false);
}
}
// Load() -> 加载公钥,验证哈希
public void load() {
// RSA 解密常规初始化操作
try {
// 读取 authkey.pub
byte[] arrayOfByte1 = CommonUtils.readAll(CommonUtils.class.getClassLoader().getResourceAsStream("resources/authkey.pub"));
// MD5 操作
byte[] arrayOfByte2 = CommonUtils.MD5(arrayOfByte1);
// 对比 hash,以校验 authkey.pub 文件是否符合要求
if (!"8bb4df00c120881a1945a43e2bb2379e".equals(CommonUtils.toHex(arrayOfByte2))) {
// 无效的授权文件
CommonUtils.print_error("Invalid authorization file");
System.exit(0);
}

X509EncodedKeySpec localX509EncodedKeySpec = new X509EncodedKeySpec(arrayOfByte1);
KeyFactory localKeyFactory = KeyFactory.getInstance("RSA");
// RSA 公钥
this.pubkey = localKeyFactory.generatePublic(localX509EncodedKeySpec);
} catch (Exception var5) {
this.error = "Could not deserialize authpub.key";
MudgeSanity.logException("authpub.key deserialization", var5, false);
}
}

public String error() {
return this.error;
}

/*
* 解密 cobaltstrike.auth 的主函数,如果返回不为 null,则校验通过。
* */
public byte[] decrypt(byte[] paramArrayOfByte) {
// RSA 解密,并返回 byte 数组
byte[] arrayOfByte1 = this._decrypt(paramArrayOfByte);

try {
if (arrayOfByte1.length == 0) {
return arrayOfByte1;
} else {
// 将解密好的数据,交给了 DataParser
DataParser localDataParser = new DataParser(arrayOfByte1);
localDataParser.big();
// byte 数组转有符号 Int -> 取头部 4个字节判断文件头是否正确,这里并不是标准的 gzip头 -> byte[] b = {-54, -2, -64, -45}
// 注:有符号数最高位为1,表示负数;最高位为0,表示正数
int i = localDataParser.readInt();
if (i == -889274181) {
this.error = "pre-4.0 authorization file. Run update to get new file";
return new byte[0];
} else if (i != -889274157) {
this.error = "bad header";
return new byte[0];
} else {
// 处理文件头并解压
int j = localDataParser.readShort();
byte[] arrayOfByte2 = localDataParser.readBytes(j);
return arrayOfByte2;
}
}
} catch (Exception localException) {
this.error = localException.getMessage();
return new byte[0];
}
}

/*
* 这个函数需要注意的是,代入的数据是使用 RSA 公钥进行解密的,然后返回解密后的数据。
* 因此在生成 .auth 的时候,应该使用密钥进行加密。
*/
protected byte[] _decrypt(byte[] paramArrayOfByte) {
byte[] arrayOfByte = new byte[0];
try {
if (this.pubkey == null) {
return new byte[0];
} else {
synchronized(this.cipher) {
this.cipher.init(2, this.pubkey);
arrayOfByte = this.cipher.doFinal(paramArrayOfByte);
}

return arrayOfByte;
}
} catch (Exception localException) {
this.error = localException.getMessage();
return new byte[0];
}
}
}

结合上述两个代码,如果验证通过,则可以打开客户端页面。这与 3.X 的认证大致相同。

3.4、SleevedResource 类

与 3.X 不同的是,4.X 在 Authorization() 中新增了一个新的验证 SleevedResource.Setup()。该验证的大致流程为:

1
2
3
4
5
1、使用 .auth 文件的一部分数据作为一个 key,将该 key 再进行处理拆分;
2、程序调用内置的 dll 文件;
3、读取 dll 文件,对 dll 文件进行处理拆分;
4、使用拆分的 key 分别对拆分的 dll 分别进行 hmac 摘要验证及 AES 解密。
5、最后执行 dll。

跟进 SleevedResource.Setup(arrayOfByte3); 看一看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class SleevedResource {
private static SleevedResource singleton;
private SleeveSecurity data = new SleeveSecurity();

public static void Setup(byte[] paramArrayOfByte) {
singleton = new SleevedResource(paramArrayOfByte);
}

public static byte[] readResource(String paramString) {
return singleton._readResource(paramString);
}

private SleevedResource(byte[] paramArrayOfByte) {
// 将 16 个字节的数据传入 SleeveSecurity.registerKey() 中
this.data.registerKey(paramArrayOfByte);
}

/*
* 这是一个读取文件,并对文件进行解密的函数方法
* paramString 是一个文件名(文件相对路径)
* */
private byte[] _readResource(String paramString) {
// strrep 是将 paramString 文件路径中的 resources/ 替换成 sleeve/
String str = CommonUtils.strrep(paramString, "resources/", "sleeve/");
// 替换之后实际上是读取 jar 包中 sleeve 目录下的文件,返回一个 byte[]
byte[] arrayOfByte1 = CommonUtils.readResource(str);
if (arrayOfByte1.length > 0) {
long l = System.currentTimeMillis();
// 将读取的文件 byte[] 代入解密阶段
byte[] arrayOfByte2 = this.data.decrypt(arrayOfByte1);
return arrayOfByte2;
} else {
// 不经过替换,直接读取源文件(sleeve 中不存在的文件,在 resources 中存在)
byte[] arrayOfByte3 = CommonUtils.readResource(paramString);
if (arrayOfByte3.length == 0) {
CommonUtils.print_error("Could not find sleeved resource: " + paramString + " [ERROR]");
} else {
CommonUtils.print_stat("Used internal resource: " + paramString);
}
return arrayOfByte3;
}
}
}

发现调用 SleevedResource 类的构造函数并将该 byte 数组传递给了 dns.SleeveSecurityregisterKey() 方法,继续跟进该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void registerKey(byte[] paramArrayOfByte) {
synchronized(this) {
try {

MessageDigest localMessageDigest = MessageDigest.getInstance("SHA-256");
// 首先利用我们的 array,获取了一个 digest,大小是 256
byte[] arrayOfByte1 = localMessageDigest.digest(paramArrayOfByte);
byte[] arrayOfByte2 = Arrays.copyOfRange(arrayOfByte1, 0, 16);
byte[] arrayOfByte3 = Arrays.copyOfRange(arrayOfByte1, 16, 32);
// 取了 arrayOfByte1 的 0-16 作为 AES 的加密 key
this.key = new SecretKeySpec(arrayOfByte2, "AES");
// 取了arrayOfByte1 的 16-32 作为 Hmac 的加密 key
this.hash_key = new SecretKeySpec(arrayOfByte3, "HmacSHA256");
} catch (Exception var8) {
var8.printStackTrace();
}
}
}

嗯,到这里没见到往下走的验证了,估摸着第一轮验证就结束了。

此时启动 teamserver,则会在 temserver 中看到一个错误:

1
[-] [Sleeve] Bad HMAC on xxxxx byte message from resource

注意:此验证是在调用 CS 内置 EXE/DLL 时所需要的,当验证不通过时,则出现该错误。因此当在绕过了开头的限制,则可以开启客户端,只不过功能受影响;只有该验证顺利通过,才是完全授权验证。

3.4、decrypt() 方法调用链

我们搜索该错误,在 SleeveSecurity.decrypt() 中找到。

我们依次查看调用链:

我们在 SleevedResource 中看到此 decrypt() 方法的调用。而 readResource() 则调用了 _readResource()。我们再查看关于 readResource() 的调用:

我们就此挑选比较干净的调用例子来分析。

1
2
3
4
5
6
7
8
9
10
11
protected byte[] export_dll() {
// 判断框架位数,之后传入一个文件
byte[] arrayOfByte = SleevedResource.readResource(this.x64 ? "resources/browserpivot.x64.dll" : "resources/browserpivot.dll");
String str = CommonUtils.bString(arrayOfByte);
Packer packer = new Packer();
packer.little();
packer.addShort(this.port);
int i = str.indexOf("COBALTSTRIKE");
str = CommonUtils.replaceAt(str, CommonUtils.bString(packer.getBytes()), i);
return CommonUtils.toBytes(str);
}

此时回头查看 _readResource() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* 这是一个读取文件,并对文件进行解密的函数方法
* paramString 是一个文件名(文件相对路径)
* */
private byte[] _readResource(String paramString) {
// strrep 是将 paramString 文件路径中的 resources/ 替换成 sleeve/
String str = CommonUtils.strrep(paramString, "resources/", "sleeve/");
// 替换之后实际上是读取 jar 包中 sleeve 目录下的文件
byte[] arrayOfByte1 = CommonUtils.readResource(str);
if (arrayOfByte1.length > 0) {
long l = System.currentTimeMillis();
// 将读取的文件 byte[] 代入解密阶段
byte[] arrayOfByte2 = this.data.decrypt(arrayOfByte1);
return arrayOfByte2;
} else {
// 不经过替换,直接读取源文件(sleeve 中不存在的文件,在 resources 中存在)
byte[] arrayOfByte3 = CommonUtils.readResource(paramString);
if (arrayOfByte3.length == 0) {
CommonUtils.print_error("Could not find sleeved resource: " + paramString + " [ERROR]");
} else {
CommonUtils.print_stat("Used internal resource: " + paramString);
}
return arrayOfByte3;
}
}

我们重点来看这个解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* 该函数是从 license文件中获取的 2 个 key 进行一些列的验证解密;
* 最终将加密的 dll 文件进行解密返回。
* paramArrayOfbyte 是源文件读取出来的 byte[] 数据
*/
public byte[] decrypt(byte[] paramArrayOfbyte) {
try {
// 取 paramArrayOfbyte 的开头至倒数 -16 的数据,这段数据是 dll 的主体数据
byte[] arrayOfByte1 = Arrays.copyOfRange(paramArrayOfbyte, 0, paramArrayOfbyte.length - 16);
// 取 paramArrayOfbyte 的剩下的 16 位数据。
byte[] arrayOfByte2 = Arrays.copyOfRange(paramArrayOfbyte, paramArrayOfbyte.length - 16, paramArrayOfbyte.length);
byte[] arrayOfByte3 = null;
synchronized(this) {
// 先用我们在 license中生成的 hash_key 作为密钥,对 arrayOfByte1 进行摘要计算
this.mac.init(this.hash_key);
arrayOfByte3 = this.mac.doFinal(arrayOfByte1);
}
// 取 arrayOfByte3 的前 16位数据
byte[] arrayOfByte4 = Arrays.copyOfRange(arrayOfByte3, 0, 16);
// 两两对比,如果相等,则步入 else。该对比,主要防止 dll 被篡改。
if (!MessageDigest.isEqual(arrayOfByte2, arrayOfByte4)) {
CommonUtils.print_error("[Sleeve] Bad HMAC on " + paramArrayOfbyte.length + " byte message from resource");
return new byte[0];
} else {
byte[] arrayOfByte5 = null;
synchronized(this) {
// 在对比成功后,将使用 key 对 dll主体内容数据进行 AES 解密
arrayOfByte5 = this.do_decrypt(this.key, arrayOfByte1);
}

DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(arrayOfByte5));
int i = dataInputStream.readInt();
int j = dataInputStream.readInt();
if (j >= 0 && j <= paramArrayOfbyte.length) {
byte[] var10 = new byte[j];
dataInputStream.readFully(var10, 0, j);
// 最后返回解密后的文件,以便调用
return var10;
} else {
CommonUtils.print_error("[Sleeve] Impossible message length: " + j);
return new byte[0];
}
}
} catch (Exception exception) {
exception.printStackTrace();
return new byte[0];
}
}

这个方法要在初始验证阶段是不会进行调用的,为了方便调试,直接在 registerKey() 写个调用即可,比如:

我们只是捋清整个需要认证的过程,不细讨其他的东西,因此只需要知道 cobaltstrike.auth 文件的组成和用处即可。

3.5、认证流程图

0x04 破解方法

理论上,穷举 Authorization() 中的 arrayOfByte3 有些不现实;因为逆着推回去,需要推出 hmac 的 key,AES 的 key,是我想多了。

因此,至少需要知道 arrayOfByte3 的值才可能正常运行成功。但在有这个关键 key 的前提下,我们可以这么做。

4.1、重新生成 license

也就是说,我们要伪造一个自己的授权文件的话, 只需要生成自己的 RSA 公私钥,然后使用私钥对文本内容进行加密,将公钥保存成 authpub.key ,并计算 MD5 值,对 AuthCrypto.class 中的 8bb4df00c120881a1945a43e2bb2379e 进行替换即可。这里的做法就是 Cobaltstrike 4破解之 我自己给我自己颁发license 中的做法。

4.1.1、.auth 文件组成

由对 Authorization() 的分析过程可以得出文本内容应该由这些有效元素构成:

1
2
3
4
5
6
7
8
9
10
将 .auth 文件读取成 byte[],处理之后得出 26 位的 byte[],将其拆分为:

4位 -> 经过有符号转换 int,结果为29999999 -> 用于判断是否永久有效(是否为发行版)
4位 -> 经过有符号转换 int,结果不为 0 即可 -> 水印
1位 -> 该 byte 值必须是大于 40 且小于 128 -> 判断认证是否适合 4.x
1位 -> 该 byte 值必须是 16 -> key 的长度
16位 -> 该 key 理论上无法逆推

要注意的是:处理 .auth 文件的时候,还需要判断文件头...,因此还需要填充文件头,但按照分析下来,是 4 个字节,但在实测过程中,发现是 6 个字节
因此 .auth 文件的必要数据是 32 位 -> 6 + 4 + 4 + 1 + 1 + 16

因此我们只需要逆推 DataParser 中的 readInt() 就可以得到想要的内容。

因此在解析 .auth 后返回的 byte[] 应该为:

1
byte[] decrypt = { 1, -55, -61, 127, 0, 0, 34, -112, 127, 16, 27, -27, -66, 82, -58, 37, 92, 51, 85, -114, -118, 28, -74, 103, -53, 6 };

代入测试:

4.1.2、生成 RSA 公钥、私钥及签名

该步骤,主要是生成 RSA 公私钥,然后使用私钥对上述生成的数据进行加密(注意,当你用私钥加密的时候,需要用公钥解密)后保存到 cobaltstrike.auth中。

你可以使用 openssl 生成,使用 2048 位即可,也可以使用代码生成,参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.*;
import java.security.*;
import java.util.Base64;

public class RSAKeyPairGenerator {
private PrivateKey privateKey;
private PublicKey publicKey;

public RSAKeyPairGenerator() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair pair = keyGen.generateKeyPair();
this.privateKey = pair.getPrivate();
this.publicKey = pair.getPublic();
}

// 将byte 写入文件
public void byte2File(String path, byte[] data) throws IOException {
File f = new File(path);
f.getParentFile().mkdirs();

FileOutputStream fos = new FileOutputStream(f);
fos.write(data);
fos.flush();
fos.close();
}

public PrivateKey getPrivateKey() {
return privateKey;
}

public PublicKey getPublicKey() {
return publicKey;
}

// 加密数据
public byte[] encryptPri(byte[] data, PrivateKey privateKey) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, this.privateKey);
return cipher.doFinal(data);
}

public static void main(String[] args) throws NoSuchAlgorithmException, IOException, IllegalBlockSizeException, InvalidKeyException, NoSuchPaddingException, BadPaddingException {
RSAKeyPairGenerator PairGenerator = new RSAKeyPairGenerator();
byte[] data = { -54, -2, -64, -45, 0, 43, 1, -55, -61, 127, 0, 0, 34, -112, 127, 16, 27, -27, -66, 82, -58, 37, 92, 51, 85, -114, -118, 28, -74, 103, -53, 6 };
byte[] rsaByte = PairGenerator.encryptPri(data, PairGenerator.getPrivateKey());
PairGenerator.byte2File("RSA/cobaltstrike.auth", rsaByte);
PairGenerator.byte2File("RSA/authkey.private", PairGenerator.getPrivateKey().getEncoded());
PairGenerator.byte2File("RSA/authkey.pub", PairGenerator.getPublicKey().getEncoded());
}
}

成功生成,剩下的就是替换相关文件,再更改 AuthCrypto.load() 中的 MD5 值。

4.2、硬编码 key

直接在 Authorization() 中注释掉以下代码行:

1
2
byte[] arrayOfByte2 = authCrypto.decrypt(arrayOfByte1);
注释部分可以扩大到读取 cobaltstrike.auth 部分

然后直接将解析后的 byte[] 进行写入

1
byte[] arrayOfByte2 = { 1, -55, -61, 127, 0, 0, 34, -112, 127, 16, 27, -27, -66, 82, -58, 37, 92, 51, 85, -114, -118, 28, -74, 103, -53, 6 };

4.3、CSHook.jar

以上两种方法都对 Jar 包进行修改,那我们再来看看不对源码进行修改的前提下进行 hook。Hook 的原理就是热替换,热替换的核心就在于 Instrumentation 的两个方法:

1
2
3
4
5
// addTransformer() 用来注册类的修改器;
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

// retransformClasses() 会让类重新加载,从而使得注册的类修改器能够重新修改类的字节码。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

这里主要是使用了 addTransformer(),其实原理很简单,就是将 4.2 中编译好的 Authorization() 类进行热替换,从而不去修改 jar 包的情况下完成认证。这部分知识可以参考:javaagent使用指南

4.3.1、读取 Authorization.class

首先先读取改写好的 Authorization.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 先读取 Authorization.class,byte[] 转 base64
public void toByteArray(String filename) throws IOException{
File f = new File(filename);
if (!f.exists()) {
throw new FileNotFoundException(filename);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream((int) f.length());
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(f));
int buf_size = 1024;
byte[] buffer = new byte[buf_size];
int len = 0;
while (-1 != (len = in.read(buffer, 0, buf_size))) {
bos.write(buffer, 0, len);
}
String base64Str = Base64.getEncoder().encodeToString(bos.toByteArray());
System.out.println(base64Str);
//return base64Str;
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
bos.close();
}
}

然后再编写 addTransformer() 的调用类

4.3.2、Transformer 类
1
2
3
4
5
6
7
8
9
10
public class Transformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("common/Authorization")) {
String base64class = "此处为 4.3.1 小节生成的内容";
System.out.println("Found desired class: " + className);
classfileBuffer = Base64.getDecoder().decode(base64class);
}
return classfileBuffer;
}
}
4.3.3、premain
1
2
3
4
5
6
7
public class CSHook {
public static void premain(String paramString, Instrumentation paramInstrumentation) {
System.out.println("Hook start");
Transformer transformer = new Transformer();
paramInstrumentation.addTransformer(transformer);
}
}

注意:指定 premain 方法的位置,这里选择了修改 META-INF/MANIFEST.MF 的内容,将 Main-Class 修改成 Premain-Class。编译生成即可。

最后,这里提供 CS 4.1 的 key :

1
byte[] arrayOfByte2 = { 1, -55, -61, 127, 0, 0, 34, -112, 127, 16, -128, -29, 42, 116, 32, 96, -72, -124, 65, -101, -96, -63, 113, -55, -86, 118 };

附件:CSHook_4.1.zip

0x05 参考

RcoIl Alipay
!坚持技术分享,您的支持将鼓励我继续创作!